#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <pthread.h>

#include "bitmap.h"

#define max(a, b) ((a)>(b))?(a):(b)
#define min(a, b) ((a)>(b))?(b):(a)

// Kernels for edge and blur filters
double Edge[] =
{
	 0, -1,  0,
	-1,  4, -1,
	 0, -1,  0
};

double Blur[] =
{
	0.0, 0.2,  0.0,
	0.2, 0.2,  0.2,
	0.0, 0.2,  0.0
};

const int filterWidth = 3;
const int filterHeight = 3;

// Description of the work assigned to a thread
typedef struct {
	int offset;
	int length;
} ParallelTask;

const int imageWidth = 240;
const int imageHeight = 320;

//
pixel_t* sourceImage;
pixel_t* frameBuffer;

void* filter(void* ptr) {

	ParallelTask* taskDescription = (ParallelTask*) ptr;
	int offset = taskDescription->offset;
	int length = taskDescription->length;

	int x, y;
	int filterX, filterY;

	for(x = offset; x < offset + length; x++) {
		for(y = 0; y < imageHeight; y++) { 

			// Edge filter
			double edge = 0.0;

			for(filterX = 0; filterX < filterWidth; filterX++) {
				for(filterY = 0; filterY < filterHeight; filterY++) {

					int imageX = (x - filterWidth / 2 + filterX + imageWidth) % imageWidth; // Wrapped

					int imageY = y - filterHeight / 2 + filterY; // Clamped
					imageY = max(imageY,0);
					imageY = min(imageY,(imageHeight-1));

					edge += sourceImage[imageX + imageY * imageWidth].R * Edge[filterX + filterY * filterWidth];
					edge += sourceImage[imageX + imageY * imageWidth].G * Edge[filterX + filterY * filterWidth];
					edge += sourceImage[imageX + imageY * imageWidth].B * Edge[filterX + filterY * filterWidth];
				}
			}

			// Check if pixel is above edge threshold
			int isEdge;

			if ((edge/3)/255 > 0.08) {
				isEdge = 1;
			} else {
				isEdge = 0;
			}

			// Filter (only on non edge pixels)
			double red = 0.0, green = 0.0, blue = 0.0;

			double distanceToCenter = (sqrt(pow((x - imageWidth / 2), 2) + pow((y - imageHeight / 2), 2)));

			double val = distanceToCenter / 255;

			double valr = (sqrt(pow((x - imageWidth/2),2) + pow((y - imageHeight/2),2))) / 255;
			double valg = (sqrt(pow((x - 50 - imageWidth/2),2) + pow((y + 75 - imageHeight/2),2))) / 255;
			double valb = (sqrt(pow((x + 50 - imageWidth/2),2) + pow((y + 75 - imageHeight/2),2))) / 255;

			if(isEdge == 0) {

				// Apply blur filter
				for(filterX = 0; filterX < filterWidth; filterX++) {
					for(filterY = 0; filterY < filterHeight; filterY++) {

						int imageX = (x - filterWidth / 2 + filterX + imageWidth) % imageWidth; // Wrapped

						int imageY = y - filterHeight / 2 + filterY; // Clamped
						imageY = max(imageY,0);
						imageY = min(imageY,(imageHeight-1));

						red   += sourceImage[imageX + imageY * imageWidth].R * Blur[filterX + filterY * filterWidth];
						green += sourceImage[imageX + imageY * imageWidth].G * Blur[filterX + filterY * filterWidth];
						blue  += sourceImage[imageX + imageY * imageWidth].B * Blur[filterX + filterY * filterWidth];
					}
				}

				// Linear interpolate between source pixel and blurred pixel
				red   = (sourceImage[x + y * imageWidth].R*(1-val) + val*red  );
				green = (sourceImage[x + y * imageWidth].G*(1-val) + val*green);
				blue  = (sourceImage[x + y * imageWidth].B*(1-val) + val*blue );

			} else {
				red   = sourceImage[x + y * imageWidth].R;
				green = sourceImage[x + y * imageWidth].G;
				blue  = sourceImage[x + y * imageWidth].B;
			}

			// Truncate values smaller than zero and larger than 255
			frameBuffer[x + y * imageWidth].R = (unsigned char) min(max((int)((1.3 + valr * 0.3) * red + 0), 0), 255);
			frameBuffer[x + y * imageWidth].G = (unsigned char) min(max((int)((1.0 + valg * 0.3) * green + 0), 0), 255);
			frameBuffer[x + y * imageWidth].B = (unsigned char) min(max((int)((0.7 + valb * 0.3) * blue + 0), 0), 255);

			// Scale intensity
			double intensityFactor = (355 - distanceToCenter) / 255;

			frameBuffer[x + y * imageWidth].R = (unsigned char) min(max((int)(intensityFactor*frameBuffer[x + y * imageWidth].R), 0), 255);
			frameBuffer[x + y * imageWidth].G = (unsigned char) min(max((int)(intensityFactor*frameBuffer[x + y * imageWidth].G), 0), 255);
			frameBuffer[x + y * imageWidth].B = (unsigned char) min(max((int)(intensityFactor*frameBuffer[x + y * imageWidth].B), 0), 255);
		}
	}

	free(ptr);
}

int main(int argc, char** argv) {

	sourceImage = (pixel_t*) malloc(imageWidth * imageHeight * sizeof(pixel_t));
	frameBuffer = (pixel_t*) malloc(imageWidth * imageHeight * sizeof(pixel_t));

	// Load the source image from file
	if (!LoadBitmapFile(argv[1], sourceImage, imageWidth, imageHeight)) {
		return -1;
	}

	// Start worker threads
	int i;

	int numberOfThreads = atoi(argv[3]);

	pthread_t threads[numberOfThreads];

	int rowsPerThread = imageWidth / numberOfThreads;

	for (i = 0; i < numberOfThreads; i++) {

		ParallelTask* taskDescription = malloc(sizeof(ParallelTask));
		taskDescription->offset = rowsPerThread * i;
		taskDescription->length = rowsPerThread;

		if (i == numberOfThreads - 1) {
			taskDescription->length += imageWidth % numberOfThreads;
		}

		pthread_create(&threads[i], NULL, filter, (void*) taskDescription);
	}

	// Join worker threads,
	// this blocks the execution of the main program until all threads are done.
	for (i = 0; i < numberOfThreads; i++) {
		pthread_join(threads[i], NULL);
	}

	// Write the output image to file
	if (!WriteBitmapFile(argv[2], frameBuffer, imageWidth, imageHeight)) {
		return -1;
	}

	free(sourceImage);
	free(frameBuffer);

	return 0;
}
